package net.mhelvens.deltaprofiles;

import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.BaseColumns;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.util.LongSparseArray;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

abstract public class Condition extends DBEntity {
    private static final String TAG = Condition.class.getSimpleName();
	
	////////// Public Methods //////////////////////////////////////////////////
	
    ///// URIs /////
    
	final public Uri uri() {
		assert isInitialized();
		
		return uri_base().buildUpon().appendPath("/"+id()) .build();
	}
	
	///// Mutators /////
	
	final public void delete() {
		assert isInitialized();
		assert !isDeleted();
		
		onDelete();
		
		DeltaProfilesDB.get().db().delete(
			TABLE_main, KEY_id+" = "+id(), null
		);
		
		mDeleted = true;
		mCache.delete( id() );
		
		broadcast(ACTION_BROADCAST_DELETE, uri());
	}
	
	///// Accessors /////
	
	final public boolean isDeleted() {
		assert isInitialized();
		
		return mDeleted;
	}
	
	final public long id() {
		assert isInitialized();
		assert !isDeleted();
		
		return mID;
	}
	
	final public String source() {
		return this.getClass().getCanonicalName();
	}

	///// Building the Formula Views /////
	
	public interface FormulaViewAdapter {
		void appendView(View v);
		void appendEditView(View v);
	}
	
	// TODO: Change this to a more conventional Android way to notify the view.
	public interface OnChanged {
		void notifyView();
	}
	
	abstract public void buildFormula(OnChanged oc, FormulaViewAdapter fva, ViewGroup container);
	
	////////// Protected  Methods //////////////////////////////////////////////

	///// Constructors /////
	
	protected Condition() {
	} // At this point: still uninitialized (intentionally empty)
	
	///// To Be Overwritten /////
	
	protected String initialData() { return ""; }
	protected void onNew()         {} // intentionally empty
	protected void onInitialize()  {} // intentionally empty
	protected void onRefresh()     {} // intentionally empty
	protected void onDelete()      {} // intentionally empty
	
	abstract protected boolean eval();
	
	protected void broadcastModify() {
		assert isInitialized();
		
		// Without the subclass, we don't know enough about
		// the data to send a more specific broadcast.
		broadcast(ACTION_BROADCAST_MODIFY, uri());
	}
	
	///// Mutators /////
	
	final protected Condition setData(String data) {
		assert isInitialized();
		assert !isDeleted();
		
		mData = data;

    	ContentValues values = new ContentValues();
    	values.put(KEY_data, data);
		DeltaProfilesDB.get().db().update(
			TABLE_main, values, KEY_id+" = "+id(), null
		);
		
		broadcastModify();
		
		return this;
	}
	
	///// Accessors /////
	
	final protected boolean isInitialized() {
		return mInitialized;
	};
	
	final protected String data() {
		assert isInitialized();
		assert !isDeleted();
		
		refresh();
		return mData;
	}
	
	////////// Package Private /////////////////////////////////////////////////
	
	///// Accessors /////
	
	final Rule rule() {
		return mRule;
	}
	
	final Condition parent() {
		return mParent;
	}
	
	final Integer childIndex() {
		return mChildIndex;
	}
	
	final IterableSparseArray<Condition> allSubconditions() {
		assert isInitialized();
		assert !isDeleted();
		
		refresh();
		
		return mSubconditions;
	}

	///// Mutators /////
	
	Condition setRule(Rule rule) {
		mRule = rule;
    	rule.setCondition(this);
		return this;
	}
	
	///// Database Constants /////
	
    //================================================================
    static final String TABLE_main      = "conditions";
    //----------------------------------------------------------------
	static final String KEY_id          = BaseColumns._ID;
    static final String KEY_source      = "source";
    static final String KEY_data        = "data";
    static final String KEY_rule        = "rule";
    static final String KEY_parent      = "parent";
    static final String KEY_child_index = "child_index";
    //================================================================
	
	////////// Private Fields //////////////////////////////////////////////////

	///// Bookkeeping /////

	private boolean mInitialized = false;
	private boolean mValid       = false;
	private boolean mDeleted     = false;
	
	///// Unique Database ID /////
	
	private long mID;
	
	///// Parent /////
	private Rule      mRule       = null;
	private Condition mParent     = null;
	private Integer   mChildIndex = null;
	
	///// Data-items and Defaults /////
	
	private String mData = null;
	private IterableSparseArray<Condition> mSubconditions = new IterableSparseArray<Condition>();
	
	////////// Private Methods /////////////////////////////////////////////////
	
	///// Mutators /////
	
	private Condition addSubcondition(int index, Condition condition) {
		mSubconditions.append(index, condition);
		condition.mChildIndex = index;
		condition.mParent     = this;
		return this;
	}
	
	private Condition initializeAsNew(Rule rule) {
		assert !isInitialized();
		
		setRule(rule);
		
		ContentValues values = initialValuesForDB();
		values.put(KEY_rule, rule.id());
		
		return initializeAsNew(values);
	}
	
	private Condition initializeAsNew(Condition parent, int index) {
		assert !isInitialized();
		
		parent.addSubcondition(index, this);
		
		ContentValues values = initialValuesForDB();
		values.put( KEY_parent     , parent.id() );
		values.put( KEY_child_index, index       );
		
		return initializeAsNew(values);
	}
	
	private Condition initializeAsNew(ContentValues values) {
		assert !isInitialized();
		
		mID = DeltaProfilesDB.get().db().insertOrThrow(
			TABLE_main, null, values
		);
    	
		mCache.put( id(), this );
    	
		onNew();
		
    	setInitialized();
    	
    	setInvalid();
		
		onInitialize();
		
		broadcast(ACTION_BROADCAST_ADD, uri());
		
		return this;
	}
	
	private Condition initializeAsExisting(long id, Cursor c) {
		assert !isInitialized();
		
		mID = id;
    	
		mCache.put( id(), this );
		
		if (c != null)
			refresh(c);
		
    	setInitialized();
		
		onInitialize();
		
		return this;
	}
	
	///// Utility Methods /////
	
	private ContentValues initialValuesForDB() {
		ContentValues result = new ContentValues();
		result.put(KEY_source, source()      );
		result.put(KEY_data  , initialData() );
		return result;
	}
	
	private void fetchInitialSubconditionsFromDB() {
		assert mSubconditions.size() == 0;
		
		Cursor c = DeltaProfilesDB.get().db().query(
			TABLE_main, null, KEY_parent+" = "+id(),
			null, null, null, KEY_child_index
		);
		
		for (c.moveToFirst(); !c.isAfterLast(); c.moveToNext()) {
			mSubconditions.put(
				c.getInt(c.getColumnIndex(KEY_child_index)),
				get(c.getLong(c.getColumnIndex(KEY_id)), c)
			);
		}
	}
	
	private void setValuesFromDB(Cursor c) {
		assert !c.isBeforeFirst() && !c.isAfterLast();
		
		mData = c.getString(c.getColumnIndex(KEY_data));
	}

	private void broadcast(String action, Uri uri) { broadcast(action, uri, null); }
	private void broadcast(String action, Uri uri, Bundle extras) {
		LocalBroadcastManager.getInstance(mContext).sendBroadcast(
			new Intent(action, uri())
				.putExtra( "id", id() )
		);
	}
	
	private void setInitialized() { mInitialized = true; }
	
	private void setInvalid() { mValid = false; }
	private void setValid()   { mValid = true ; }
	
	private void refresh() { refresh(null); }
	private void refresh(Cursor c) {
		assert !isDeleted();
		if (!mValid) {
			// is a positioned cursor supplied already? otherwise, perform query
			if (c == null) {
				c = DeltaProfilesDB.get().db().query(
						TABLE_main, null, KEY_id+" = "+id(),
						null, null, null, null
					);
				c.moveToFirst();
			} else {
				assert !c.isBeforeFirst() && !c.isAfterLast();
			}
			
			setValuesFromDB(c);
			
			fetchInitialSubconditionsFromDB();
			
			setValid();
		}
	}
	
	////////// Static //////////////////////////////////////////////////////////
	
	///// Public Methods /////
	
	static final public Uri uri_base() { return new Uri.Builder().path("/conditions").build(); }

	static final public void setContext(Context context) {
		mContext = context;
	}
	
	static final public Context context() {
		return mContext;
	}
	
	static final public Long idOf(Condition condition) {
		if (condition == null) return null;
		return condition.id();
	}
	
    static final public Condition create(String source, Rule rule)
    		throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    	Condition result = getInstance(source);
    	result.initializeAsNew(rule);
    	return result;
    }
	
    static final public Condition create(String source, Condition parent, int index)
    		throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    	Condition result = getInstance(source);
    	result.initializeAsNew(parent, index);
    	return result;
    }
    
    static final public Condition get(long id) { return get(id, null); }
    
	///// Broadcast Actions /////
	
	static public final String ACTION_BROADCAST_ADD    = "net.mhelvens.deltaprofiles.Condition.ACTION_BROADCAST_ADD";
	static public final String ACTION_BROADCAST_MODIFY = "net.mhelvens.deltaprofiles.Condition.ACTION_BROADCAST_MODIFY";
	static public final String ACTION_BROADCAST_DELETE = "net.mhelvens.deltaprofiles.Condition.ACTION_BROADCAST_DELETE";

	///// Private Utility Methods /////
	
    static private boolean isCached(long id) {
    	return mCache.get(id, null) != null;
    }
	
    static private Condition getInstance(String source)
    		throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    	try {
    		return Class.forName(source).asSubclass(Condition.class).newInstance();
		} catch (ClassNotFoundException e) {
			Log.e(TAG, "Error creating new instance (source = "+source+"); re-throwing exception.", e);
			throw e;
		}
    }
    
    static private Condition get(Long id, Cursor c) {
    	if (id == null || id <= 0)
    		return null;
    	
    	if (!isCached(id)) {
			try {
		    	if (c == null) {
		    		c = DeltaProfilesDB.get().db().query(
		    			TABLE_main, null, KEY_id+" = "+id,
		    			null, null, null, null
		    		);
		    		c.moveToFirst();
		    	}
				String source = c.getString(c.getColumnIndex(KEY_source));
				getInstance(source).initializeAsExisting(id, c);
			} catch (Exception e) {
				Log.e(TAG, "Unable to instantiate existing condition", e);
			}
    	}
    	
    	mCache.get(id).refresh(c);
    	
    	return mCache.get(id);
    }
    
	///// Private Fields /////

	static private Context                    mContext;
	static private LongSparseArray<Condition> mCache = new LongSparseArray<Condition>();
}
